Skip to content

1.3 SwiftUI Essentials(Landmarks)处理用户输入

Handling User Input — SwiftUI Tutorials | Apple Developer Documentation

在Landmarks应用程序中,用户可以标记他们最喜欢的地方,并过滤列表,只显示他们最喜欢的地方。要实现该功能,首先要在列表中添加一个开关,这样用户就可以只关注自己的最爱,然后再添加一个星形按钮,用户点击该按钮就可以将某个地标标记为最爱。

标记用户最喜欢的landmark

|400

swift
struct LandmarkRow: View {
    var landmark: Landmark
    
    var body: some View {
        HStack {
            landmark.image.resizable().frame(width: 50,height: 50)
            Text(landmark.name)
            Spacer()
            // 增加判断
            if landmark.isFavorite {
                Image(systemName: "star.fill")
                    .foregroundColor(.yellow)
            }
        }
    }
}
struct LandmarkRow: View {
    var landmark: Landmark
    
    var body: some View {
        HStack {
            landmark.image.resizable().frame(width: 50,height: 50)
            Text(landmark.name)
            Spacer()
            // 增加判断
            if landmark.isFavorite {
                Image(systemName: "star.fill")
                    .foregroundColor(.yellow)
            }
        }
    }
}

过滤用户最喜欢的landmark

  1. 增加状态标记是否需要进行过滤
swift
import SwiftUI
struct LandmarkList: View {    
    // 由于您使用状态属性来保存特定于视图及其子视图的信息,因此以 private 的形式创建状态。
    @State
    private var showFavoriteOnly = false
    
    var filteredLandmarks:[Landmark]  {
        landmarks.filter { item in
            return showFavoriteOnly ? item.isFavorite : true
        }
    }
    
    var body: some View {
        NavigationView {
            List(filteredLandmarks) { item in
                NavigationLink {
                    LandmarkDetail(landmark: item)
                } label: {
                    LandmarkRow(landmark: item)
                }

            }.navigationTitle("landmark")
        }
    }
}
import SwiftUI
struct LandmarkList: View {    
    // 由于您使用状态属性来保存特定于视图及其子视图的信息,因此以 private 的形式创建状态。
    @State
    private var showFavoriteOnly = false
    
    var filteredLandmarks:[Landmark]  {
        landmarks.filter { item in
            return showFavoriteOnly ? item.isFavorite : true
        }
    }
    
    var body: some View {
        NavigationView {
            List(filteredLandmarks) { item in
                NavigationLink {
                    LandmarkDetail(landmark: item)
                } label: {
                    LandmarkRow(landmark: item)
                }

            }.navigationTitle("landmark")
        }
    }
}

2). 增加toggle控件,控制当前是否需要过滤。为了直接把Toggle放到list中去,改用ForEach进行遍历显示组件。

swift
import SwiftUI

struct LandmarkList: View {    
    // 由于您使用状态属性来保存特定于视图及其子视图的信息,因此以 private 的形式创建状态。
    @State
    private var showFavoriteOnly = false    
    var filteredLandmarks:[Landmark]  {
        landmarks.filter { item in
            return showFavoriteOnly ? item.isFavorite : true
        }
    }    
    var body: some View {
        NavigationView {
//            List(landmarks) { item in
//                NavigationLink {
//                    LandmarkDetail(landmark: item)
//                } label: {
//                    LandmarkRow(landmark: item)
//                }
//            }.navigationTitle("landmark")
            List{
                Toggle(isOn: $showFavoriteOnly) {
                    Text("Favorites only")
                }
                ForEach(filteredLandmarks) { landmark in
                    NavigationLink {
                        LandmarkDetail(landmark: landmark)
                    } label: {
                        LandmarkRow(landmark: landmark)
                    }
                }
            }.navigationTitle("landmark")
        }
    }
}
import SwiftUI

struct LandmarkList: View {    
    // 由于您使用状态属性来保存特定于视图及其子视图的信息,因此以 private 的形式创建状态。
    @State
    private var showFavoriteOnly = false    
    var filteredLandmarks:[Landmark]  {
        landmarks.filter { item in
            return showFavoriteOnly ? item.isFavorite : true
        }
    }    
    var body: some View {
        NavigationView {
//            List(landmarks) { item in
//                NavigationLink {
//                    LandmarkDetail(landmark: item)
//                } label: {
//                    LandmarkRow(landmark: item)
//                }
//            }.navigationTitle("landmark")
            List{
                Toggle(isOn: $showFavoriteOnly) {
                    Text("Favorites only")
                }
                ForEach(filteredLandmarks) { landmark in
                    NavigationLink {
                        LandmarkDetail(landmark: landmark)
                    } label: {
                        LandmarkRow(landmark: landmark)
                    }
                }
            }.navigationTitle("landmark")
        }
    }
}

使用Observable对象进行存储

为了让用户把自己喜欢的地标放入收藏夹,首先需要将地标landmark数据放入到可观察(Observable)对象中。

可观察对象是您的数据的自定义对象,可从SwiftUI环境中的存储绑定到视图。SwiftUI会监视对可观察对象的任何可能影响视图的更改,并在更改后显示正确版本的视图。

1). 修改数据源

swift
import Combine
import Foundation

// var landmarks: [Landmark] = load("landmarkData.json")
// SwiftUI订阅(subscribes)您的可观察(observable)对象,并在数据变化时更新任何需要刷新的视图。
final class ModelData: ObservableObject {
    // 可观察对象需要发布对其数据的任何更改,以便其订阅者能够接收更改。
    @Published var landmarks:[Landmark] = load("landmarkData.json")
}

func load<T: Decodable>(_ filename: String) -> T {
    let data: Data

    guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
    else {
        fatalError("Couldn't find \(filename) in main bundle.")
    }

    do {
        data = try Data(contentsOf: file)
    } catch {
        fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
    }

    do {
        let decoder = JSONDecoder()
        return try decoder.decode(T.self, from: data)
    } catch {
        fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
    }
}
import Combine
import Foundation

// var landmarks: [Landmark] = load("landmarkData.json")
// SwiftUI订阅(subscribes)您的可观察(observable)对象,并在数据变化时更新任何需要刷新的视图。
final class ModelData: ObservableObject {
    // 可观察对象需要发布对其数据的任何更改,以便其订阅者能够接收更改。
    @Published var landmarks:[Landmark] = load("landmarkData.json")
}

func load<T: Decodable>(_ filename: String) -> T {
    let data: Data

    guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
    else {
        fatalError("Couldn't find \(filename) in main bundle.")
    }

    do {
        data = try Data(contentsOf: file)
    } catch {
        fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
    }

    do {
        let decoder = JSONDecoder()
        return try decoder.decode(T.self, from: data)
    } catch {
        fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
    }
}

2). 页面使用模型对象:LandmarkList页面

@ EnvironmentObject是一种属性包装器,用于将环境对象注入到视图层次结构中的任何地方,并使其可用于所有层次结构中的子视图。环境对象是一个可观察的对象,可以包含任意类型的数据,并且可以在整个应用程序中共享和使用,例如应用程序的设置或用户身份验证。@EnvironmentObject属性包装器的使用涉及以下步骤:

  1. 创建一个ObservableObject的自定义类并放入一些需要跨视图共享的属性。
  2. 在应用程序的顶层视图中使用@EnvironmentObject注册此自定义类。
  3. 在需要访问环境对象的任何视图中使用@EnvironmentObject属性包装器获取访问这个环境对象的能力
swift
import SwiftUI

struct LandmarkList: View {

    @EnvironmentObject var modelData: ModelData
    
    // 由于您使用状态属性来保存特定于视图及其子视图的信息,因此以 private 的形式创建状态。
    @State
    private var showFavoriteOnly = false
    
    var filteredLandmarks:[Landmark]  {
        // landmarks.filter { item in
        modelData.landmarks.filter { item in
            return showFavoriteOnly ? item.isFavorite : true
        }
    }
    
    var body: some View {
        NavigationView {
            List{
                Toggle(isOn: $showFavoriteOnly) {
                    Text("Favorites only")
                }
                ForEach(filteredLandmarks) { landmark in
                    NavigationLink {
                        LandmarkDetail(landmark: landmark)
                    } label: {
                        LandmarkRow(landmark: landmark)
                    }
                }
            }.navigationTitle("landmark")
        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        // LandmarkList()
        // 使用的是 .environmentObject 初始化
        LandmarkList().environmentObject(ModelData())
    }
}
import SwiftUI

struct LandmarkList: View {

    @EnvironmentObject var modelData: ModelData
    
    // 由于您使用状态属性来保存特定于视图及其子视图的信息,因此以 private 的形式创建状态。
    @State
    private var showFavoriteOnly = false
    
    var filteredLandmarks:[Landmark]  {
        // landmarks.filter { item in
        modelData.landmarks.filter { item in
            return showFavoriteOnly ? item.isFavorite : true
        }
    }
    
    var body: some View {
        NavigationView {
            List{
                Toggle(isOn: $showFavoriteOnly) {
                    Text("Favorites only")
                }
                ForEach(filteredLandmarks) { landmark in
                    NavigationLink {
                        LandmarkDetail(landmark: landmark)
                    } label: {
                        LandmarkRow(landmark: landmark)
                    }
                }
            }.navigationTitle("landmark")
        }
    }
}

struct LandmarkList_Previews: PreviewProvider {
    static var previews: some View {
        // LandmarkList()
        // 使用的是 .environmentObject 初始化
        LandmarkList().environmentObject(ModelData())
    }
}

修改contentView,这里用到了@StateObject:

使用 @StateObject 属性只能在应用程序的生命周期中为给定属性初始化模型对象一次。在应用程序实例中使用该属性(如图所示)以及在视图中使用该属性时都是如此。

相关名词解释(来自chatgpt):
@StateObject 是一个属性包装器,用于将对象作为 State 在视图中进行管理。SwiftUI中,每当属性的值更改时,所有依赖该值的视图都会重新计算和更新,@StateObject 通过将其包装的对象作为State来管理其状态,并确保只创建一个实例。当对象的状态改变时,SwiftUI会自动重新计算视图。

使用 @StateObject 的好处是可以将对象的生命周期委托给 SwiftUI 管理,也可以方便地在不同的视图之间共享相同的实例。这对于需要使用长时间存在的对象(例如网络请求、数据库连接或其他长时间运行的任务)的视图非常有用。

swift
struct ContentView: View {

    @StateObject private var modelData = ModelData()
    
    var body: some View {
       // LandmarkList()
        LandmarkList().environmentObject(modelData)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
struct ContentView: View {

    @StateObject private var modelData = ModelData()
    
    var body: some View {
       // LandmarkList()
        LandmarkList().environmentObject(modelData)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

通过创建“收藏按钮”进行数据交互

1). 创建收藏按钮

swift
import SwiftUI
struct FavoriteButton: View {   

    @Binding var isSet: Bool    
    
    var body: some View {
        Button {
            // 使用该方法将布尔值从true切换为false,或从false切换为true。
            isSet.toggle()
        } label: {
            Label("Toggle Favorite", systemImage: isSet ? "star.fill" : "star")
                            .labelStyle(.iconOnly)
                            .foregroundColor(isSet ? .yellow : .gray)
        }
    }
}

struct FavoriteButton_Previews: PreviewProvider {
    static var previews: some View {
        FavoriteButton(isSet: .constant(true))
    }
}
import SwiftUI
struct FavoriteButton: View {   

    @Binding var isSet: Bool    
    
    var body: some View {
        Button {
            // 使用该方法将布尔值从true切换为false,或从false切换为true。
            isSet.toggle()
        } label: {
            Label("Toggle Favorite", systemImage: isSet ? "star.fill" : "star")
                            .labelStyle(.iconOnly)
                            .foregroundColor(isSet ? .yellow : .gray)
        }
    }
}

struct FavoriteButton_Previews: PreviewProvider {
    static var previews: some View {
        FavoriteButton(isSet: .constant(true))
    }
}

2). 增加交互

swift
import SwiftUI

struct LandmarkDetail: View {
    var landmark:Landmark

    @EnvironmentObject var modelData:ModelData
    var landmarkIndex: Int {
        modelData.landmarks.firstIndex(where: { $0.id == landmark.id })!
    }
    
    var body: some View {
        ScrollView {
            MapView(coordinates: landmark.locationCoordinate)
                .ignoresSafeArea()
                .frame(height: 300)

            CircleImage(image: landmark.image)
                .offset(y: -180)
                .padding(.bottom, -180)
            
            VStack(alignment: .leading) {
                HStack {
                    Text(landmark.name)
                        .font(.title)
                    .foregroundColor(.cyan)                    
                    FavoriteButton(isSet: $modelData.landmarks[landmarkIndex].isFavorite)
                }
                HStack {
                    Text(landmark.park)
                        .font(.subheadline)
                    Spacer()
                    Text(landmark.state)
                        .font(.subheadline)
                }
                Divider()
                Text("About \(landmark.name)").font(.title2)
                Text(landmark.description).font(.subheadline)
            }
        }.navigationTitle(landmark.name)
            .navigationBarTitleDisplayMode(.inline)
    }
}

struct LandmarkDetail_Previews: PreviewProvider {
    static let modelData = ModelData()

    static var previews: some View {
        LandmarkDetail(landmark: ModelData().landmarks[1])
            .environmentObject(modelData)
    }
}
import SwiftUI

struct LandmarkDetail: View {
    var landmark:Landmark

    @EnvironmentObject var modelData:ModelData
    var landmarkIndex: Int {
        modelData.landmarks.firstIndex(where: { $0.id == landmark.id })!
    }
    
    var body: some View {
        ScrollView {
            MapView(coordinates: landmark.locationCoordinate)
                .ignoresSafeArea()
                .frame(height: 300)

            CircleImage(image: landmark.image)
                .offset(y: -180)
                .padding(.bottom, -180)
            
            VStack(alignment: .leading) {
                HStack {
                    Text(landmark.name)
                        .font(.title)
                    .foregroundColor(.cyan)                    
                    FavoriteButton(isSet: $modelData.landmarks[landmarkIndex].isFavorite)
                }
                HStack {
                    Text(landmark.park)
                        .font(.subheadline)
                    Spacer()
                    Text(landmark.state)
                        .font(.subheadline)
                }
                Divider()
                Text("About \(landmark.name)").font(.title2)
                Text(landmark.description).font(.subheadline)
            }
        }.navigationTitle(landmark.name)
            .navigationBarTitleDisplayMode(.inline)
    }
}

struct LandmarkDetail_Previews: PreviewProvider {
    static let modelData = ModelData()

    static var previews: some View {
        LandmarkDetail(landmark: ModelData().landmarks[1])
            .environmentObject(modelData)
    }
}